﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Web.Mvc;
using CodeStash.Common.DataAccess.AspMembership;
using CodeStash.Common.DataAccess.EntityFramework;
using CodeStash.Common.DataAccess.Repository;
using CodeStash.Common.DataAccess.UnitOfWork;
using CodeStash.Common.DTOS;
using CodeStash.Common.Encryption;
using CodeStash.Common.Enums;
using CodeStash.Common.Helpers;
using CodeStash.Filters;
using CodeStash.Services;
using CodeStash.Utils;
using System.Web;



namespace CodeStash.Controllers
{
    public class RestController : Controller
    {
        #region Data
        private readonly IGetUserForRestService getUserForRestService;
        private readonly ILoggerService loggerService;
        private readonly IRepository<CodeSnippet> codeSnippetRepository;
        private readonly IRepository<CodeCategory> codeCategoryRepository;
        private readonly IRepository<CodeTag> codeTagRepository;
        private readonly IRepository<Grouping> groupingRepository;
        private readonly IRepository<Language> languageRepository;
        private readonly IRepository<CreatedTeam> createdTeamRepository;
        private readonly IRepository<Visibility> visibilityRepository;
        private readonly IUnitOfWork unitOfWork;
        #endregion

        #region Ctor

        public RestController(IGetUserForRestService getUserForRestService,
                                ILoggerService loggerService,
                                IRepository<CodeSnippet> codeSnippetRepository,
                                IRepository<CodeCategory> codeCategoryRepository,
                                IRepository<CodeTag> codeTagRepository,
                                IRepository<Grouping> groupingRepository,
                                IRepository<Language> languageRepository,
                                IRepository<CreatedTeam> createdTeamRepository,
                                IRepository<Visibility> visibilityRepository,
                                IUnitOfWork unitOfWork)
        {
            this.getUserForRestService = getUserForRestService;
            this.loggerService = loggerService;
            this.codeSnippetRepository = codeSnippetRepository;
            this.codeCategoryRepository = codeCategoryRepository;
            this.codeTagRepository = codeTagRepository;
            this.groupingRepository = groupingRepository;
            this.languageRepository = languageRepository;
            this.createdTeamRepository = createdTeamRepository;
            this.visibilityRepository = visibilityRepository;
            this.unitOfWork = unitOfWork;
        }
        #endregion

        #region REST API
        /// <summary>
        /// Searches all available <c>CodeSnippet</c>(s) and returns a List<c>JSONPagesSearchResultCodeSnippet</c>(s)
        /// </summary>
        [JSONInput(Param = "input", RootType = typeof(JSONSearchInput))]
        public ActionResult Search(JSONSearchInput input)
        {
            try
            {
                Guard.AssertNotNull<JSONSearchInput>(input, "Input");
                Guard.AssertStringNotNull(input.Email, "Email");
                Guard.AssertGreaterOrEqual((int)input.SearchType, 0, "SearchType");
                Guard.AssertGreaterOrEqual(input.PageNo, 0, "PageNo");
                Guard.AssertGreaterOrEqual(input.PageSize, 0, "PageSize");
                Guard.AssertGreaterOrEqual((int)input.Visibility, 0, "Visbility");

                if(input.IncludeRatings)
                    Guard.AssertGreaterOrEqual(input.Rating, 1, "Rating");

                switch (input.SearchType)
                {
                    case SearchType.ByKeyWord:
                        Guard.AssertStringNotNull(input.SearchValue, "SearchValue");
                        break;
                    case SearchType.ByTag:
                        Guard.AssertNotNull<string[]>(input.Tags, "tags");
                        break;
                }


                StringBuilder sb = new StringBuilder();
                sb.Append("Rest : Running search using the following paramaters : ");
                sb.AppendFormat("Email={0}", input.Email);
                sb.AppendFormat("SearchType={0}", input.SearchType);
                sb.AppendFormat("PageNo={0}", input.PageNo);
                sb.AppendFormat("PageSize={0}", input.PageSize);
                sb.AppendFormat("Visibility={0}", input.Visibility);
                sb.AppendFormat("SearchValue={0}", input.SearchValue);
                if (input.Tags.Count() > 0)
                {
                    sb.AppendFormat("Tags={0}", input.Tags.Aggregate((x, y) => string.Format("{0};{1}", x, y)));
                }
                loggerService.Info(sb.ToString());


                UserResults userFromResults = GetUserFromResults(input.CodeStashToken, input.Email, input.Password);
                if (!userFromResults.SucessfullyFetched)
                {
                    loggerService.Error(string.Format("Rest : An error occurred whilst trying to conduct search \r\n{0}", userFromResults.ExceptionMessage));
                    return Json(new JSONPagesSearchResultCodeSnippet(0, null, userFromResults.ExceptionMessage));
                }
                else
                {
                    Tuple<int, List<CodeSnippet>> pagedMatchedSnippets = null;
                    JSONPagesSearchResultCodeSnippet results = null;

                    using (unitOfWork)
                    {
                        pagedMatchedSnippets = SearchUtils.FilterByVisibility(
                                                    unitOfWork,
                                                    createdTeamRepository,
                                                    codeTagRepository,
                                                    languageRepository,
                                                    codeSnippetRepository,
                                                    input.SearchType,
                                                    input.SearchValue.ToLower(),
                                                    input.PageNo,
                                                    input.PageSize,
                                                    true,
                                                    input.Visibility,
                                                    userFromResults.AspMembershipUser.UserId,
                                                    input.Tags,
                                                    input.IncludeRatings,
                                                    input.Rating);


                        if (pagedMatchedSnippets != null)
                        {

                            //now translate into JSON objects
                            List<JSONCodeSnippet> jsonSnippets = new List<JSONCodeSnippet>();

                            if (pagedMatchedSnippets.Item2.Count > 0)
                            {
                                codeCategoryRepository.EnrolInUnitOfWork(unitOfWork);
                                languageRepository.EnrolInUnitOfWork(unitOfWork);
                                groupingRepository.EnrolInUnitOfWork(unitOfWork);
                                codeTagRepository.EnrolInUnitOfWork(unitOfWork);
                            }

                            foreach (CodeSnippet efSnippet in pagedMatchedSnippets.Item2)
                            {
                                List<CodeTag> actualTags = codeTagRepository.FindBy(t => t.MatchingSnippetId == efSnippet.CodeSnippetId).ToList();

                                StringBuilder tagBuilder = new StringBuilder();
                                foreach (CodeTag tag in actualTags)
                                {
                                    tagBuilder.Append(String.Format("{0};", tag.Description));
                                }
                                string tagsComplete = tagBuilder.ToString();
                                if (tagsComplete != string.Empty && tagsComplete.TrimEnd().EndsWith(";"))
                                {
                                    tagsComplete = tagsComplete.TrimEnd().Substring(0, tagsComplete.Length - 1);
                                }

                                //Due to Entity Framework only being able to load one Include when Lazy loading
                                //we need to load up all related entities using a lookup against the database
                                CodeCategory category = codeCategoryRepository.FindBy(x => x.CodeCategoryId == efSnippet.CodeCategoryId).Single();
                                Language language = languageRepository.FindBy(x => x.LanguageId == efSnippet.LanguageId).Single();
                                Grouping grouping = null;
                                if (efSnippet.GroupId.HasValue)
                                {
                                    grouping = groupingRepository.FindBy(x => x.GroupId == efSnippet.GroupId.Value).Single();
                                }


                                jsonSnippets.Add(ConvertToJSONCodeSnippet(efSnippet, category, language, grouping, tagsComplete));
                            }

                            loggerService.Error(string.Format("Rest : Found {0} snippets that matched your query, returning paged data", pagedMatchedSnippets.Item1));
                            results = new JSONPagesSearchResultCodeSnippet(pagedMatchedSnippets.Item1, jsonSnippets, string.Empty);
                        }
                        else
                        {
                            loggerService.Error("Rest : Could not find any Snippets that matched your search");
                            results = new JSONPagesSearchResultCodeSnippet(0, null, "Could not find any Snippets that matched your search");
                        }
                    }

                    return Json(results);
                }
            }
            catch (Exception ex)
            {
                loggerService.Error(string.Format("Rest : An error occurred whilst trying to conduct search \r\n{0}", ex.Message));
                return Json(new JSONPagesSearchResultCodeSnippet(0, null, ex.Message));
            }
        }



       /// <summary>
        /// Adds a new <c>CodeSnippet</c> and returns a <c>JSONCodeSnippetAddSingleResult</c>
        /// with the newly added <c>CodeSnippet</c> or an Exception that may have occurred
        /// </summary>
        [JSONInput(Param = "input", RootType = typeof(JSONAddSnippetInput))]
        [HttpPost, ValidateInput(false)]
        public ActionResult AddSnippet(JSONAddSnippetInput input)
        {
            try
            {
                Guard.AssertNotNull<JSONAddSnippetInput>(input, "Input");
                Guard.AssertStringNotNull(input.Email, "Email");
                Guard.AssertNotNull<JSONCodeSnippet>(input.CodeSnippetToAdd, "CodeSnippetToAdd");
                Guard.AssertStringNotNull(input.CodeSnippetToAdd.ActualCode, "ActualCode");
                Guard.AssertStringNotNull(input.CodeSnippetToAdd.CategoryName, "CategoryName");
                Guard.AssertStringNotNull(input.CodeSnippetToAdd.Description, "Description");
                Guard.AssertStringNotNull(input.CodeSnippetToAdd.Title, "Title");
                Guard.AssertGreaterOrEqual(input.CodeSnippetToAdd.LanguageId, 0, "LanguageId");
                Guard.AssertGreaterOrEqual((int)input.CodeSnippetToAdd.Visibility, 0, "Visbility");

                JSONCodeSnippet snippetToAdd = input.CodeSnippetToAdd;


                if (input.SnippetIsPreHtmlEncoded)
                {
                    //Decode if we need to
                    snippetToAdd = new JSONCodeSnippet(
                        HttpUtility.HtmlDecode(input.CodeSnippetToAdd.ActualCode),
                        input.CodeSnippetToAdd.CategoryId,
                        HttpUtility.HtmlDecode(input.CodeSnippetToAdd.CategoryName),
                        input.CodeSnippetToAdd.LanguageId,
                        HttpUtility.HtmlDecode(input.CodeSnippetToAdd.Language),
                        HttpUtility.HtmlDecode(input.CodeSnippetToAdd.Tags),
                        HttpUtility.HtmlDecode(input.CodeSnippetToAdd.Description),
                        HttpUtility.HtmlDecode(input.CodeSnippetToAdd.Title),
                        input.CodeSnippetToAdd.GroupId,
                        HttpUtility.HtmlDecode(input.CodeSnippetToAdd.GroupName),
                        input.CodeSnippetToAdd.Visibility,0,0,0);
                }



                StringBuilder sb = new StringBuilder();
                sb.Append("Rest : Attempting to add snippet using the following paramaters : ");
                sb.AppendFormat("Email={0}", input.Email);
                sb.AppendFormat("ActualCode={0}...", snippetToAdd.ActualCode.Take(50));
                sb.AppendFormat("CategoryName={0}", snippetToAdd.CategoryName);
                sb.AppendFormat("Description={0}", snippetToAdd.Description);
                sb.AppendFormat("Title={0}", snippetToAdd.Title);
                sb.AppendFormat("LanguageId={0}", snippetToAdd.LanguageId);
                sb.AppendFormat("Visbility={0}", snippetToAdd.Visibility);
                sb.AppendFormat("Tags={0}", !String.IsNullOrEmpty(snippetToAdd.Tags) ? snippetToAdd.Tags : "");
                loggerService.Info(sb.ToString());



                UserResults userFromResults = GetUserFromResults(input.CodeStashToken, input.Email, input.Password);
                if (!userFromResults.SucessfullyFetched)
                {
                    loggerService.Error(string.Format("Rest : An error occurred whilst trying to add snippet \r\n{0}", userFromResults.ExceptionMessage));
                    return Json(new JSONCodeSnippetAddSingleResult(null, userFromResults.ExceptionMessage));
                }
                else
                {
                    CodeSnippet codeSnippet = null;
                    JSONCodeSnippet jsonCodeSnippet = null;

                    using (unitOfWork)
                    {
                        codeCategoryRepository.EnrolInUnitOfWork(unitOfWork);
                        groupingRepository.EnrolInUnitOfWork(unitOfWork);
                        languageRepository.EnrolInUnitOfWork(unitOfWork);
                        visibilityRepository.EnrolInUnitOfWork(unitOfWork);


                        #region Look up or insert category
                        CodeCategory category = null;

                        IEnumerable<CodeCategory> categories;

                        if (string.IsNullOrEmpty(snippetToAdd.CategoryName))
                        {
                            categories = new List<CodeCategory>();
                        }
                        else
                        {
                            categories = codeCategoryRepository.FindBy(x => x.CodeCategoryName.Trim() == snippetToAdd.CategoryName);
                        }


                        if (categories.Any())
                        {
                            category = categories.First();
                        }
                        else
                        {
                            category = new CodeCategory(snippetToAdd.CategoryName.Trim());
                            codeCategoryRepository.Add(category);
                        }
                        #endregion

                        #region Look up or insert group
                        //NOTE : grouping are not mandaatory, so it may be possible to get to end of this
                        //       section and still not create a Grouping, that is total valid
                        Grouping grouping = null;


                        IEnumerable<Grouping> groupings;

                        if (string.IsNullOrEmpty(snippetToAdd.GroupName))
                        {
                            groupings = new List<Grouping>();
                        }
                        else
                        {
                            groupings = groupingRepository.FindBy(x => x.Description.Trim() == snippetToAdd.GroupName.Trim());
                        }

                        if (groupings.Any())
                        {
                            grouping = groupings.First();
                        }
                        else
                        {
                            if (!String.IsNullOrEmpty(snippetToAdd.GroupName))
                            {
                                grouping = new Grouping(snippetToAdd.GroupName.Trim());
                                groupingRepository.Add(grouping);
                            }
                        }
                        #endregion

                        //Look up language
                        Language language = languageRepository.FindBy(x => x.LanguageId == snippetToAdd.LanguageId).First();

                        //Look up visibility
                        Visibility visibility = visibilityRepository.FindBy(x => x.Id == (int)snippetToAdd.Visibility).First();


                        //commit the potentially added entities, which will give us the Id's of the inserted items
                        unitOfWork.Commit();

                        //now enrol in new work cycle, where we can use the Id's of the entities we just added above
                        codeSnippetRepository.EnrolInUnitOfWork(unitOfWork);

                        Nullable<int> groupingForSnippet = null;
                        if (grouping != null)
                            groupingForSnippet = grouping.GroupId;



                        #region Insert CodeSnippet
                        codeSnippet = new CodeSnippet(
                            snippetToAdd.ActualCode,
                            category.CodeCategoryId,
                            snippetToAdd.Description,
                            snippetToAdd.Title,
                            groupingForSnippet,
                            language.LanguageId,
                            userFromResults.AspMembershipUser.UserId,
                            visibility,
                            snippetToAdd.Views,
                            snippetToAdd.Rating,
                            snippetToAdd.TotalRatings,
                            DateTime.Now);


                        codeSnippetRepository.Add(codeSnippet);

                        //commit the potentially added entities, which will give us the Id's of the inserted items
                        unitOfWork.Commit();

                        #endregion

                        #region Add tags is there are any
                        if (!String.IsNullOrEmpty(snippetToAdd.Tags))
                        {
                            //now enrol in new work cycle, where we can use the Id's of the entities we just added above
                            codeTagRepository.EnrolInUnitOfWork(unitOfWork);

                            //For each tag specified associate with the new CodeSnippet
                            string[] tags = snippetToAdd.Tags.Split(new char[] { ';' });
                            foreach (string tag in tags)
                            {
                                codeTagRepository.Add(new CodeTag(tag, codeSnippet.CodeSnippetId));
                            }
                            unitOfWork.Commit();
                        }
                        #endregion



                        jsonCodeSnippet = ConvertToJSONCodeSnippet(codeSnippet, category, language, grouping, snippetToAdd.Tags);
                    }
                    loggerService.Info("Rest : Sucessfully added snippet");

                    JSONCodeSnippetAddSingleResult jsonResult = new JSONCodeSnippetAddSingleResult(jsonCodeSnippet, "");
                    JsonResult jsonifiedData = Json(jsonResult);
                    jsonifiedData.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
                    return jsonifiedData;

                }


            }
            catch (Exception ex)
            {
                loggerService.Error(string.Format("Rest : An error occurred whilst trying to add snippet search \r\n{0}", ex.Message));
                return Json(new JSONCodeSnippetAddSingleResult(null, ex.Message));
            }
        }

        /// <summary>
        /// Gets all <c>Language</c>(s) which are returned in a <c>JSONLanguages</c>
        /// with all the <c>Language</c>(s) or an Exception that may have occurred
        /// </summary>
        [JSONInput(Param = "input", RootType = typeof(JSONCredentialInput))]
        public ActionResult GetAllLanguages(JSONCredentialInput input)
        {
            try
            {
                Guard.AssertNotNull<JSONCredentialInput>(input, "Input");
                Guard.AssertStringNotNull(input.Email, "Email");

                UserResults userFromResults = GetUserFromResults(input.CodeStashToken, input.Email, input.Password);
                if (!userFromResults.SucessfullyFetched)
                {
                    loggerService.Error(string.Format("Rest : An error occurred whilst trying to obtain all languages\r\n{0}", userFromResults.ExceptionMessage));
                    return Json(new JSONLanguagesResult(null, userFromResults.ExceptionMessage));
                }
                else
                {
                    List<Language> languages = new List<Language>();

                    using (unitOfWork)
                    {
                        languageRepository.EnrolInUnitOfWork(unitOfWork);
                        languages = languageRepository.FindAll().OrderBy(x => x.Language1).ToList();
                    }

                    List<JSONLanguage> jsonLanguages = new List<JSONLanguage>();
                    foreach (Language language in languages)
                    {
                        jsonLanguages.Add(new JSONLanguage(language.LanguageId, language.Language1));
                    }


                    loggerService.Error("Rest : Sucessfully obtained all languages");
                    JsonResult result = Json(new JSONLanguagesResult(jsonLanguages, ""));
                    return result;
                }
            }
            catch (Exception ex)
            {
                loggerService.Error(string.Format("Rest : An error occurred whilst trying to obtain all languages\r\n{0}", ex.Message));
                return Json(new JSONLanguagesResult(null, ex.Message));
            }
        }


        /// <summary>
        /// Gets all <c>Grouping</c>(s) which are returned in a <c>JSONGrouping</c>
        /// with all the <c>Grouping</c>(s) or an Exception that may have occurred
        /// </summary>
        [JSONInput(Param = "input", RootType = typeof(JSONCredentialInput))]
        public ActionResult GetAllGroups(JSONCredentialInput input)
        {
            try
            {
                Guard.AssertNotNull<JSONCredentialInput>(input, "Input");
                Guard.AssertStringNotNull(input.Email, "Email");

                UserResults userFromResults = GetUserFromResults(input.CodeStashToken, input.Email, input.Password);
                if (!userFromResults.SucessfullyFetched)
                {
                    loggerService.Error(string.Format("Rest : An error occurred whilst trying to obtain all groupings\r\n{0}", userFromResults.ExceptionMessage));
                    return Json(new JSONGroupingResult(null, userFromResults.ExceptionMessage));
                }
                else
                {
                    List<Grouping> groupings = new List<Grouping>();

                    using (unitOfWork)
                    {
                        groupingRepository.EnrolInUnitOfWork(unitOfWork);
                        groupings = groupingRepository.FindAll().ToList();
                    }

                    List<JSONGrouping> jsonGroupings = new List<JSONGrouping>();
                    foreach (Grouping grouping in groupings)
                    {
                        jsonGroupings.Add(new JSONGrouping(grouping.GroupId, grouping.Description));
                    }

                    loggerService.Error("Rest : Sucessfully obtained all groupings");
                    return Json(new JSONGroupingResult(jsonGroupings, ""));
                }
            }
            catch (Exception ex)
            {
                loggerService.Error(string.Format("Rest : An error occurred whilst trying to obtain all groupings\r\n{0}", ex.Message));
                return Json(new JSONGroupingResult(null, ex.Message));
            }
        }

        #endregion

        #region Private Helper Methods


        private JSONCodeSnippet ConvertToJSONCodeSnippet(
            CodeSnippet efSnippet,
            CodeCategory codeCategory,
            Language language,
            Grouping grouping,
            string rawTagData)
        {
            JSONCodeSnippet jsonSnippet = new JSONCodeSnippet(
                        efSnippet.CodeSnippetId,
                        efSnippet.ActualCode,
                        efSnippet.CodeCategoryId,
                        codeCategory.CodeCategoryName.Trim(),
                        efSnippet.LanguageId,
                        language.Language1,
                        rawTagData,
                        efSnippet.Description,
                        efSnippet.Title,
                        efSnippet.GroupId.HasValue ? efSnippet.GroupId : null,
                        grouping != null ? grouping.Description.Trim() : "",
                        (CodeSnippetVisibility)efSnippet.Visibility,
                        efSnippet.Views,
                        efSnippet.Rating,
                        efSnippet.TotalRatings);
            return jsonSnippet;
        }


        private string GetDeSerializedJsonValue(string jsonString)
        {
            using (var ms = new MemoryStream(Encoding.Unicode.GetBytes(jsonString)))
            {
                return (String)new DataContractJsonSerializer(typeof(String)).ReadObject(ms);
            }
        }


        /// <summary>
        /// Attempts to fetch a user from the Membership database using the
        /// GetUserForRestService which internally makes use of the "sp_GetUserForRest" stored 
        /// procedure. This method returns a <c>UserResults</c> object which has 3 values which
        /// are the results of the fetch operation
        /// </summary>
        private UserResults GetUserFromResults(string codeStashToken, string email, string password)
        {

            //try fetching user using encrypted values
            List<RESTUser> userMatches = getUserForRestService.GetUserForRest(codeStashToken, email, password);
            Tuple<RESTUser, String> validationOfUser = ValidateFetchedUser(userMatches, loggerService);
            if (validationOfUser.Item1 != null)
            {
                return new UserResults(true, validationOfUser.Item1, "");
            }
            else
            {
                //fall back to trying with decrypted values
                DecryptedUserValues decryptedValues = EncryptionHelper.GetDecryptedValues(codeStashToken,
                    email, password);
                userMatches = getUserForRestService.GetUserForRest(decryptedValues.DecryptedCodeStashToken,
                    decryptedValues.DecryptedEmail, decryptedValues.DecryptedPassword);

                validationOfUser = ValidateFetchedUser(userMatches, loggerService);
                if (validationOfUser.Item1 != null)
                {
                    return new UserResults(true, validationOfUser.Item1, "");
                }
                else
                {
                    loggerService.Error(string.Format("Could not obtain correct user for Email : {0}", email));
                    return new UserResults(false, null, validationOfUser.Item2);
                }
            }
        }


        private static Tuple<RESTUser, String> ValidateFetchedUser(List<RESTUser> userMatches, ILoggerService loggerService)
        {
            if (userMatches == null)
            {
                loggerService.Error("Could not find any user matching those criteria");
                return new Tuple<RESTUser, string>(null, "Could not find any user matching those criteria");
            }
            else
            {
                if (userMatches.Count == 1)
                {
                    return new Tuple<RESTUser, string>(userMatches.First(), String.Empty);
                }
                else
                {
                    loggerService.Error("Too many users were found with those criteria");
                    return new Tuple<RESTUser, string>(null, "Too many users were found with those criteria");
                }
            }
        }
        #endregion
    }
}